Prismaのコネクションプール関連のパラメータについて確認してみた

Prismaのコネクションプール関連のパラメータについて確認してみた

Clock Icon2024.11.13

リテールアプリ共創部@大阪の岩田です。

先日Prismaのコネクションプール周りのパラメータについて調査する機会がありました。せっかくなので調査した内容についてご紹介します。以後の内容は全てMySQL利用の前提となります。

環境

今回利用したPrismaのバージョンは以下の通りです。

  • clientVersion: 5.15.0
  • engineVersion: 12e25d8d06f6ea5a0252864dd9a03b1bb51f3022

Prismaでコネクションプール関連の設定を調整するには?

Prismaでコネクションプール関連の設定を調節する場合は接続文字列にパラメータを指定します。イメージはこんな感じです。

'mysql://prisma:[email protected]:3306/mysql?<パラメータ1>=<パラメータ1の値>&<パラメータ2>=<パラメータ2の値>'

大抵の場合は環境変数DATABASE_URLを使って設定していると思います。

さっそく具体的なパラメータ値について見ていきましょう。

connection_limit

このパラメータはコネクションプールのサイズ上限を指定します。デフォルト値は物理CPU数×2 + 1です。

デフォルト値を計算しているのはこの辺です。

https://github.com/prisma/prisma-engines/blob/62bc04885206eec0143f1111c72d1a5e85692f7c/quaint/src/pooled.rs#L194

プール内のコネクション数がconnection_limitに達している状態でさらにクライアントからコネクションのチェックアウトがリクエストされると、対象のリクエストはブロックされることになります。

コンビニやスーパーのレジでイメージするとこんな感じです。

コンビニのレジを例に考えるconnection_limit

余談ですが、今回の調査のきっかけはあるシステムに対して実施した負荷テストです。レスポンスタイムの悪化や500エラーの発生が見られたもののDBセッションはわずかしか消費していなかったため、コネクションプール周りの設定が怪しそうだと考えました。そのシステムではconnection_limitが特に設定されておらずデフォルト値が採用していたため、重めのSQLが実行されるとすぐに処理が詰まっていたようです。

以下のコードを実行して簡単に検証してみましょう。Promise.allSettledを使って1秒SleepするSQLの発行を5並行で実行するような処理になります。connection_limitには5を指定しているため、コネクションプールのサイズ上限は5です。

const prisma  = require('@prisma/client');
const client   = new prisma.PrismaClient();

process.env.DATABASE_URL='mysql://prisma:[email protected]:3306/mysql?connection_limit=5'

const query = async () => {
    return client.$queryRaw`SELECT  SLEEP(1)`
        .then(()=> console.log((new Date()).toISOString()))
}

Promise.allSettled([
    query(),
    query(),
    query(),
    query(),
    query(),
]).then(console.log)

実行結果は以下の通りです。

2024-11-12T11:16:25.974Z
2024-11-12T11:16:25.986Z
2024-11-12T11:16:25.987Z
2024-11-12T11:16:25.987Z
2024-11-12T11:16:25.987Z
[
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined }
]

5つのクエリがほぼ同時に完了していることがわかります。

該当時間帯のinformation_schema.processlist;の出力は以下の通りでした。

mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user            | info                                                    |
+----+-----------------+---------------------------------------------------------+
|  8 | root            | select id,user,info from information_schema.processlist |
| 16 | prisma          | SELECT  SLEEP(1)                                        |
| 17 | prisma          | SELECT  SLEEP(1)                                        |
|  5 | event_scheduler | NULL                                                    |
| 13 | prisma          | SELECT  SLEEP(1)                                        |
| 14 | prisma          | SELECT  SLEEP(1)                                        |
| 15 | prisma          | SELECT  SLEEP(1)                                        |
+----+-----------------+---------------------------------------------------------+
7 rows in set (0.00 sec)

PrismaのコネクションプールからDBに対して5つのセッションが張られていることがわかります。

今度は接続文字列を指定している箇所のconnection_limitを1に書き換えて再実行してみましょう。

出力は以下の通りでした。

2024-11-12T11:19:53.884Z
2024-11-12T11:19:54.890Z
2024-11-12T11:19:55.895Z
2024-11-12T11:19:56.899Z
2024-11-12T11:19:57.904Z
[
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined },
  { status: 'fulfilled', value: undefined }
]

今度は約1秒ごとに日時が出力されていることがわかります。コネクションプールのサイズが小さいためうまく並行にDBアクセスできず、直列実行のような状態になっていることが分かります。

information_schema.processlistの確認結果は以下の通りでした。

mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user            | info                                                    |
+----+-----------------+---------------------------------------------------------+
|  8 | root            | select id,user,info from information_schema.processlist |
| 18 | prisma          | SELECT  SLEEP(1)                                        |
|  5 | event_scheduler | NULL                                                    |
+----+-----------------+---------------------------------------------------------+
3 rows in set (0.00 sec)

mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user            | info                                                    |
+----+-----------------+---------------------------------------------------------+
|  8 | root            | select id,user,info from information_schema.processlist |
| 18 | prisma          | SELECT  SLEEP(1)                                        |
|  5 | event_scheduler | NULL                                                    |
+----+-----------------+---------------------------------------------------------+
3 rows in set (0.00 sec)

mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user            | info                                                    |
+----+-----------------+---------------------------------------------------------+
|  8 | root            | select id,user,info from information_schema.processlist |
| 18 | prisma          | SELECT  SLEEP(1)                                        |
|  5 | event_scheduler | NULL                                                    |
+----+-----------------+---------------------------------------------------------+
3 rows in set (0.01 sec)

PrismaのコネクションプールからDBに対して1つのセッションしか張られていないことがわかります。

pool_timeout

このパラメータはコネクションプールからコネクションをチェックアウトする際の待ち時間上限を指定します。クライアントからconnection_limitを超える接続要求があった場合、対象のリクエストはコネクションが利用可能になるまでブロックされるのですが、この値を超えるとあきらめてErrorがthrowされます。

イメージはこんな感じ

コンビニのレジを例に考えるpool_timeout

デフォルト値は10となっており、以下の箇所で指定されています。

https://github.com/prisma/prisma-engines/blob/62bc04885206eec0143f1111c72d1a5e85692f7c/quaint/src/connector/mysql/url.rs#L150

検証用コードの接続文字列にpool_timeout=1を追加してテストしてみましょう。結果は以下のようになります。

node test
2024-11-12T11:49:48.630Z
[
  {
    status: 'rejected',
    reason: PrismaClientKnownRequestError:
    Invalid `prisma.$queryRaw()` invocation:

    Timed out fetching a new connection from the connection pool. More info: http://pris.ly/d/connection-pool (Current connection pool timeout: 1, connection limit: 1)
        at In.handleRequestError (/Users/...略/node_modules/@prisma/client/runtime/library.js:122:6877)
        at In.handleAndLogRequestError (/Users/...略/node_modules/@prisma/client/runtime/library.js:122:6211)
        at In.request (/Users/...略/node_modules/@prisma/client/runtime/library.js:122:5919)
        at async l (/Users/...略/node_modules/@prisma/client/runtime/library.js:127:11167)
        at async Promise.allSettled (index 1) {
      code: 'P2024',
      clientVersion: '5.15.0',
      meta: [Object]
    }
...略

5つ並行で呼び出したクエリのうち1つだけが成功し、残り4つはTimed out fetching a new connection from the connection poolというエラーで失敗しています。

ドキュメントに記載されていないパラメータ

ここまではPrismaのドキュメントに記載のあるパラメータなのですが、実は記事執筆時点ではドキュメントに記載されていないパラメータも存在します。これらのパラメータについては以下のissueでも言及されています。

https://github.com/prisma/docs/issues/1657

Prisma Enginesは内部的にmobcを利用しているのですが、Prismaの接続文字列に渡されたオプションは最終的にmobcのコネクションプールを作成する際のパラメータとして利用されます。MySQLの場合は以下の箇所で接続文字列がパースされ...

https://github.com/prisma/prisma-engines/blob/29bc8ca375232bc314ff30d684056b8c5a8f0583/quaint/src/connector/mssql/url.rs#L244

パースされたパラメータを使って以下の箇所でmobcのコネクションプールがビルドされます

https://github.com/prisma/prisma-engines/blob/29bc8ca375232bc314ff30d684056b8c5a8f0583/quaint/src/pooled.rs#L327-L335

ということで以後はドキュメントに記載されていないパラメータについて検証してみます。

max_idle_connection_lifetime

このパラメータはプール内でアイドル状態になっているコネクションの生存期間を設定するパラメータで、指定した値がmobcのmax_idle_lifetimeに設定されます。イメージはこんな感じ。

コンビニのレジを例に考えるmax_idel_connection_lifetiime

mobcのソースコードのコメントによると生存期間を過ぎた過ぎたコネクションは即座にクローズされるわけではなく、クローズ処理は遅延実行されるようです。

https://github.com/importcjj/mobc/blob/523cccf413e91daa040d212ea0be051f167d8be3/src/config.rs#L148-L151

NodeのインタプリタからPrismaClientを利用してクエリを発行しながら動作確認してみましょう。

まずインタプリタから以下を入力します。max_idle_connection_lifetimeには1を指定しています。

const prisma  = require('@prisma/client');
const client   = new prisma.PrismaClient();

process.env.DATABASE_URL='mysql://prisma:[email protected]:3306/mysql?max_idle_connection_lifetime=1'

const query = async () => {
    return client.$queryRaw`SELECT  SLEEP(1)`
        .then(()=> console.log((new Date()).toISOString()))
}

この状態でinformation_schema.processlistを確認すると以下の通りでした。まだPrismaClientからの接続は存在しません。

mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
Empty set (0.00 sec)

Nodeのインタプリタからquery()を実行してから再度information_schema.processlistを確認してみましょう。

mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now()               | id | user   | info |
+---------------------+----+--------+------+
| 2024-11-13 02:33:24 | 14 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)

id=14でPrismaClient向けの接続がオープンしています。

数秒経過後に再度確認してみます。

mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now()               | id | user   | info |
+---------------------+----+--------+------+
| 2024-11-13 02:33:49 | 14 | prisma | NULL |
+---------------------+----+--------+------+

max_idle_connection_lifetimeで指定した秒数経過後ですが、相変わらずid=14の接続がオープンしている状態です。

再度query()を実行してから確認します。

mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now()               | id | user   | info |
+---------------------+----+--------+------+
| 2024-11-13 02:34:08 | 15 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)

今度はidが15となっており、先程までとは別の接続がオープンしていることが分かります。query()実行時の裏でプール内のコネクションのクローズと新規オープンが動いていることが分かります。

max_connection_lifetime

このパラメータはプール内のコネクションの生存期間を設定するパラメータで、指定した値がmobcのmax_lifetimeに設定されます。max_idle_connection_lifetimeとの違いは対象のコネクションが非アイドル状態であってもクローズ対象となることです。

イメージはこんな感じ。

コンビニのレジを例に考えるmax_connection_lifetime

こちらのパラメータについても以下のコードで動作確認してみましょう。

const prisma  = require('@prisma/client');
const client   = new prisma.PrismaClient();

process.env.DATABASE_URL='mysql://prisma:[email protected]:3306/mysql?max_idle_connection_lifetime=15&max_connection_lifetime=50'

const query = async () => {
    return client.$queryRaw`SELECT  SLEEP(1)`
        .then(()=> console.log((new Date()).toISOString()))
}

setInterval(query, 10000)

max_idle_connection_lifetimeには15を設定していますが、setIntervalを使って10秒毎にクエリが発行されるためアイドル時間は約10秒となり、15秒以上アイドル状態になることはありません。このコードを実行しながらinformation_schema.processlistを確認してみましょう。

mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now()               | id | user   | info |
+---------------------+----+--------+------+
| 2024-11-13 02:46:59 | 16 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)

まずid=16の接続がオープンしました。

mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now()               | id | user   | info |
+---------------------+----+--------+------+
| 2024-11-13 02:47:40 | 16 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)

約40秒後の状況です。max_connection_lifetimeで指定された50秒は経過していないため、id=16の接続がオープンし続けています。

mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now()               | id | user   | info |
+---------------------+----+--------+------+
| 2024-11-13 02:47:58 | 17 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.01 sec)

約1分後の状況です。max_connection_lifetimeで指定された期間を経過しているためid=16の接続はクローズされ、id=17の接続がオープンしていることが分かります。

まとめ

ドキュメントに記載されていないパラメータ値についてもmobcのコード等を参考に動作確認してみました。今回紹介したパラメータ以外にも接続文字列で色々なパラメータが指定できるので、興味がある方はPrisma Enginesのソースコードを覗いてみるのも楽しいと思います。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.